package com.cskaoyan.mall.promo.controller;

import com.cskaoyan.mall.common.constant.RedisConst;
import com.cskaoyan.mall.common.result.Result;
import com.cskaoyan.mall.common.util.AuthContext;
import com.cskaoyan.mall.common.util.MD5;
import com.cskaoyan.mall.order.dto.OrderTradeDTO;
import com.cskaoyan.mall.order.query.OrderInfoParam;
import com.cskaoyan.mall.promo.api.dto.SeckillGoodsDTO;
import com.cskaoyan.mall.promo.constant.LocalStockStatus;
import com.cskaoyan.mall.promo.constant.SeckillCodeEnum;
import com.cskaoyan.mall.promo.converter.SeckillGoodsConverter;
import com.cskaoyan.mall.promo.model.SeckillGoods;
import com.cskaoyan.mall.promo.service.PromoService;
import com.cskaoyan.mall.promo.util.LocalCacheHelper;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.List;

/**
 * @Author Pudding
 * @Date 2024/7/22 14:52
 */
@RestController
public class PromoController {

    @Autowired
    PromoService promoService;
    @Autowired
    SeckillGoodsConverter seckillGoodsConverter;
    @Autowired
    RedissonClient redissonClient;

    /**
     * 方便本地测试
     */
    @GetMapping("/init/cache")
    public Result initCache() {
        promoService.importIntoCache();
        return Result.ok();
    }

    @GetMapping("/clear/cache")
    public Result clearCache() {
        promoService.clearRedisCache();
        return Result.ok();
    }


    /**
     * 1. 前端点击秒杀，获取当天所有的秒杀商品列表
     * @return
     */
    @GetMapping("/seckill")
    public Result findAll() {

        List<SeckillGoodsDTO> seckillGoodsDTOS = promoService.findAll();
        return Result.ok(seckillGoodsDTOS);
    }

    /**
     * 2. 获取秒杀商品详细信息
     * @param skuId
     * @return
     */
    @GetMapping("seckill/{skuId}")
    public Result getSeckillGoods(@PathVariable("skuId") Long skuId) {

        // 获取秒杀商品数据详情
        SeckillGoodsDTO seckillGoodsDTO = promoService.getSeckillGoodsDTO(skuId);
        return Result.ok(seckillGoodsDTO);
    }


    /**
     * 4. 点击秒杀商品详情页的立即抢购，首先会发一个请求，获取下单码
     * 1. 下单码是什么：其实就是一个独一无二的字符串，相当于是一个令牌，有这个令牌，才能秒杀下单，没有这个令牌，不能参与秒杀下单
     * 2. 下单码的作用：主要是为了防止非法请求秒杀下单
     * 3. 下单码的工作流程：
     *      3.1 在点击立即抢购之后，前端发请求给后端，后端收到请求，生成一个独一无二的下单码返回给前端
     *      3.2 前端收到下单码，继续发起订单确认页的请求
     *      3.3 后端收到订单确认页的请求，请求中就会携带之前颁发的下单码：接下来后端首先校验下单码，
     *          如果合法，就返回订单确认页数据，让用户可以参与秒杀下单
     *          如果非法，或者下单码过期了，那么就不让用户参与秒杀下单，拒绝用户请求
     * @param skuId
     * @param request
     * @return
     */
    @GetMapping("seckill/auth/getSeckillSkuIdStr/{skuId}")
    public Result getSeckillSkuIdStr(@PathVariable("skuId") Long skuId, HttpServletRequest request) {

        //1. 获取用户id
        String userId = AuthContext.getUserId(request);

        //2. 校验
        //2.1 获取秒杀商品信息
        SeckillGoodsDTO seckillGoodsDTO = promoService.getSeckillGoodsDTO(skuId);
        if (seckillGoodsDTO != null){

            //2.2 校验当前商品在不在该商品的秒杀活动时间之内
            Date startTime = seckillGoodsDTO.getStartTime();
            Date endTime = seckillGoodsDTO.getEndTime();
            Date currentTime = new Date();

            //getTime()方法返回自1970年1月1日00:00:00 UTC至调用此方法时的毫秒数。这个时间值是一个long类型的数值
            if (currentTime.getTime() >= startTime.getTime() && currentTime.getTime() <= endTime.getTime()) {
                //3.1 生成下单码
                //方式一：生成一个uuid，然后把uuid和userId作为key-value存入redis，然后把uuid作为下单码返回给前端
                //后续在校验的时候，就可以从请求中获取下单码，然后作为key，从redis中获取value，如果有，说明下单码合法

                //方式二：对userId和skuId拼接后进行MD5散列（或其他加密算法AES），然后作为下单码返回给前端
                //后续在校验的时候，再次去通过userId和skuId拼接散列得到一个下单码，然后拿这个下单吗和请求中携带的下单码进行比对，如果一致，说明请求中的下单码合法
                String skuIdEncrypt = MD5.encrypt(userId + skuId);
                return Result.ok(skuIdEncrypt);
            }
        }

        //3.2 返回获取下单码失败
        return Result.build(null, SeckillCodeEnum.SECKILL_ILLEGAL);
    }


    /**
     * 5. 获取商品订单确认页数据/获取秒杀结算数据
     *    a) 校验下单码
     *    b) 校验本地库存状态位
     *    c) 获取秒杀商品订单确认页面
     * @param skuId
     * @param skuIdStr
     * @param request
     * @return
     */
    @GetMapping("/seckill/auth/trade/{skuId}")
    public Result<OrderTradeDTO> trade(@PathVariable("skuId") Long skuId, String skuIdStr, HttpServletRequest request) {

        //1. 校验下单码（抢购码规则可以自定义，和之前下单码构建规则一致即可）
        String userId = AuthContext.getUserId(request);
        String skuIdEncrypt = MD5.encrypt(userId + skuId);
        if (!skuIdEncrypt.equals(skuIdStr)){
            return Result.build(null, SeckillCodeEnum.SECKILL_ILLEGAL);
        }

        //2. 校验本地库存状态位
        Object stockFlag = LocalCacheHelper.get(String.valueOf(skuId));
        if (stockFlag == null || !stockFlag.equals(LocalStockStatus.HAS_STOCK.getNum())){
            return Result.build(null, SeckillCodeEnum.SECKILL_FINISH);
        }

        //3. 获取秒杀商品订单结算/确认页信息
        SeckillGoodsDTO seckillGoodsDTO = promoService.getSeckillGoodsDTO(skuId);
        SeckillGoods seckillGoods = seckillGoodsConverter.convertSeckillDTO(seckillGoodsDTO);
        OrderTradeDTO orderTradeDTO = promoService.getTradeData(userId, seckillGoods);

        return Result.ok(orderTradeDTO);
    }

    /**
     * 6. 提交秒杀订单
     * 秒杀的下单，和普通的订单下单，有如下区别：
     *     1. 普通订单在生成的时候，不会扣减库存，只会判断有没有对应的库存
     *     2. 秒杀订单在下单的时候，就会扣减库存，在支付的时候，不会再次减库存了
     *     3. 普通订单的超时取消，不会回补对应的商品的库存，因为在下单的时候没有减库存
     *     4. 秒杀订单的超时取消，会回补对应的商品的库存，因为在下单的时候已经减库存了
     *
     * tip：因为秒杀订单是在下单的时候，就扣减库存，那么就存在用户下了单，但是没有付钱，导致少卖，尽量不少卖 所以超时时间设置比普通订单更短一些
     *
     * @param orderInfoParam
     * @param request
     * @return
     * @RequestBody表示接受请求体中的参数数据
     */
    @PostMapping("/seckill/auth/submitOrder")
    public Result submitOrder(@RequestBody OrderInfoParam orderInfoParam, HttpServletRequest request){

        //1. 获取用户id
        String userId = AuthContext.getUserId(request);

        //2. 判断一下是否有库存（根据本地库存状态位标记进行判断）
        Long skuId = orderInfoParam.getOrderDetailList().get(0).getSkuId();
        if (!LocalCacheHelper.get(String.valueOf(skuId)).equals(LocalStockStatus.HAS_STOCK.getNum())){
            //返回下单失败
            return Result.build(null, SeckillCodeEnum.SECKILL_FINISH);
        }

        //3. 判断一下用户是否已经抢过了（一个用户一般只能抢购一个商品）
        //获取key
        String key = RedisConst.PROMO_USER_ORDERED_FLAG + userId;
        // 在这里，利用了Redis的set数据结构不可重复的特点
        RSet<Object> set = redissonClient.getSet(key);  //获取redisson中的set集合
        // 添加商品id到集合中；如果集合中已经存在了这个商品id，此时会返回false  说明用户之前已经买过这个商品了
        // 如果集合中不存在这个商品id，那么会返回true，说明用户之前没有购买过这个商品
        boolean flag = set.tryAdd(skuId);
        if (!flag){
            //返回下单失败
            return Result.build(null, SeckillCodeEnum.SECKILL_DUPLICATE_TRADE);
        }

        //4. 减扣秒杀商品库存, 生成秒杀订单
        //给前端请求参数orderInfoParam中的userId设置
        orderInfoParam.setUserId(Long.valueOf(userId));

        /*
        4.1 方式一：没有使用分布式事务控制，不能保证扣减库存和生成订单的一致性
        boolean orderFlag = promoService.submitOrder(orderInfoParam);
        if (!orderFlag){
            //返回下单失败
            return Result.build(null, SeckillCodeEnum.SECKILL_FINISH);
        }
        5. 返回下单成功
        return Result.build(null, SeckillCodeEnum.SECKILL_ORDER_SUCCESS);
        */

        //4.2 方式二：使用事务消息控制，保证扣减库存（秒杀服务中）和生成订单（订单服务中)的最终一致性
        promoService.submitOrderInTransaction(orderInfoParam);

        //5. 返回正在排队中
        //说明此时秒杀库存扣减成功，但是秒杀订单还没有生成，在这里不要给用户返回下单成功，而是返回正在排队中，否则用户可能看不到自己秒杀订单
        return Result.build(null, SeckillCodeEnum.SECKILL_RUN);
    }


    /**
     * 7. 前端发现code=201时，也就是正在排队中，每隔3秒发一个定时任务轮询检查后端的消费结果，也就是检查订单的生成情况
     * @param skuId
     * @param request
     * @return
     */
    @GetMapping(value = "/seckill/auth/checkOrder/{skuId}")
    public Result checkOrder(@PathVariable("skuId") Long skuId, HttpServletRequest request) {

        //1. 获取用户id
        String userId = AuthContext.getUserId(request);

        //2. 检查当前用户购买skuId这个商品的订单有没有生成，通过查询redis中的promo:orders:这个标记，这个标记是在order中的秒杀消费者订单生成之后放入进去的
        boolean orderFlag = promoService.checkOrder(skuId, userId);

        //3. 结果判断
        //3.1 如果检查到了订单生成，返回下单成功
        if (orderFlag){
            return Result.build(null, SeckillCodeEnum.SECKILL_ORDER_SUCCESS);
        }
        //3.2 如果检查到了订单没有生成，返回正在排队中，让前端继续来定时检查
        return Result.build(null, SeckillCodeEnum.SECKILL_RUN);

    }
}
